/*
  author Sylvain Bertrand <sylvain.bertrand@gmail.com>
  Protected by linux GNU GPLv2
  Copyright 2012-2014
*/
#include <linux/module.h>
#include <linux/device.h>
#include <asm/unaligned.h>
#include <linux/mutex.h>
#include <linux/slab.h>

#include <alga/amd/atombios/atb.h>
#include <alga/amd/atombios/pp.h>

#include "interpreter.h"

#include "tables/atb.h"
#include "tables/cmd.h"
#include "tables/data.h"
#include "tables/i2c.h"
#include "tables/pp.h"
#include "tables/firmware_info.h"

#include "atb.h"
#include "vm.h"

long atb_pp_defaults_get(struct atombios *atb, struct atb_pp_defaults *d)
{
	u16 of;
	struct master_data_tbl *data_tbl;
	struct firmware_info_v2_2 *info;
	long r;

	mutex_lock(&atb->mutex);

	/* engine and memory default clocks */
	of = get_unaligned_le16(&atb->hdr->master_data_tbl_of);
	data_tbl = atb->adev.rom + of;

	of = get_unaligned_le16(&data_tbl->list.firmware_info);
	info = atb->adev.rom + of;

	d->eng_clk = get_unaligned_le32(&info->default_eng_clk);
	d->mem_clk = get_unaligned_le32(&info->default_mem_clk);
	d->vddc_mv = get_unaligned_le16(&info->default_vddc);
	d->vddci_mv = get_unaligned_le16(&info->default_vddci);
	d->mvddc_mv = get_unaligned_le16(&info->default_mvddc);

	dev_info(atb->adev.dev, "atombios:firmware_info (0x%04x) revision %u.%u\n",
			of, info->hdr.tbl_fmt_rev, info->hdr.tbl_content_rev);
	if (info->hdr.tbl_fmt_rev != 2 && info->hdr.tbl_content_rev != 2) {
		dev_err(atb->adev.dev, "atombios:firmware_info revision not supported");
		r = -ATB_ERR;
		goto unlock_mutex;
	}
	dev_info(atb->adev.dev, "atombios:firmware revision 0x%08x\n",
				get_unaligned_le32(&info->firmware_rev));
	dev_info(atb->adev.dev, "atombios:default engine clock %ukHz\n",
							d->eng_clk * 10);
	dev_info(atb->adev.dev, "atombios:default memory clock %ukHz\n",
							d->mem_clk * 10);
	dev_info(atb->adev.dev, "atombios:default vddc %umV\n", d->vddc_mv);
	dev_info(atb->adev.dev, "atombios:default vddci %umV\n", d->vddci_mv);
	dev_info(atb->adev.dev, "atombios:default mvddc %umV\n", d->mvddc_mv);

	r = 0;

unlock_mutex:
	mutex_unlock(&atb->mutex);
	return r;
}
EXPORT_SYMBOL_GPL(atb_pp_defaults_get);

struct pp_parse {
	union pp *pp;
	struct pp_state_array *state_array;
	struct pp_desc_array *desc_array;
	struct pp_clk_array *clk_array;
};

static void lvl_process(struct pp_parse *pp_parse, struct pp_state *s,
				u8 lvl_idx, struct atb_pp_state *atb_pp_state)
{
	u32 clk_of;
	struct pp_clk *clk;
	struct atb_pp_lvl *lvl;

	clk_of = pp_parse->clk_array->entry_sz * s->clk_idxes[lvl_idx];
	clk = (struct pp_clk*)((u8*)&pp_parse->clk_array->clks[0] + clk_of);

	lvl = &atb_pp_state->lvls[lvl_idx];

	lvl->eng_clk = get_unaligned_le16(&clk->eng_clk_low);
	lvl->eng_clk |= clk->eng_clk_high << 16;
	lvl->mem_clk = get_unaligned_le16(&clk->mem_clk_low);
	lvl->mem_clk |= clk->mem_clk_high << 16;

	lvl->vddc_id = get_unaligned_le16(&clk->vddc_id);
	lvl->vddci_mv = get_unaligned_le16(&clk->vddci_mv);

	lvl->pcie_gen = clk->pcie_gen;
}

static long state_process(struct pp_parse *pp_parse, struct pp_state *s,
					struct atb_pp_state *atb_pp_state)
{
	u8 lvl;

	lvl = 0;
	while(1) {
		if ((lvl == s->lvls_n) || lvl == ATB_PP_STATE_LVLS_N_MAX)
			break;

		lvl_process(pp_parse, s, lvl, atb_pp_state);
		++lvl;
	}
	atb_pp_state->lvls_n = lvl;
	return 0;
}

static void state_get(struct pp_parse *pp_parse,
		struct atb_pp_state *atb_pp_state,
		u8 (*state_selector)(struct pp_parse *, struct pp_state *))
{
	u8 state_idx;
	u32 state_of;

	memset(atb_pp_state, 0, sizeof(*atb_pp_state));

	state_idx = 0;
	state_of = 0;
	while(1) {
		struct pp_state *s;

		if (state_idx == pp_parse->state_array->n)
			break;

		s = (struct pp_state*)(&pp_parse->state_array->states[0]
								+ state_of);
		if (state_selector(pp_parse, s)) {
			state_process(pp_parse, s, atb_pp_state);
			break;
		}
		++state_idx;
		state_of += sizeof(*s) + s->lvls_n * sizeof(s->clk_idxes[0]);
	}
}

static u8 is_emergency_state(struct pp_parse *pp_parse, struct pp_state *s)
{
	u16 class_0;
	u16 d_of;
	struct pp_desc *d;

	d_of = s->desc_idx * pp_parse->desc_array->entry_sz;
	d = (struct pp_desc*)((u8*)&pp_parse->desc_array->descs[0] + d_of);

	class_0 = get_unaligned_le16(&d->class_0);

	if (class_0 & PP_DESC_CLASS_0_EMERGENCY)
		return 1;
	return 0;
}

long atb_pp_emergency_state_get(struct atombios *atb,
						struct atb_pp_state *emergency)
{
	u16 of;
	struct master_data_tbl *data_tbl;
	struct pp_parse pp_parse;
	long r;

	mutex_lock(&atb->mutex);

	of = get_unaligned_le16(&atb->hdr->master_data_tbl_of);
	data_tbl = atb->adev.rom + of;

	of = get_unaligned_le16(&data_tbl->list.pp_info);
	pp_parse.pp = atb->adev.rom + of;

	dev_info(atb->adev.dev, "atombios:pp_info (0x%04x) revision %u.%u\n",
					of, pp_parse.pp->pp0.hdr.tbl_fmt_rev,
					pp_parse.pp->pp0.hdr.tbl_content_rev);

	if (pp_parse.pp->pp0.hdr.tbl_fmt_rev != 6
				&& pp_parse.pp->pp0.hdr.tbl_content_rev != 1) {
		dev_err(atb->adev.dev, "atombios:pp_info (0x%04x) revision %u.%u not supported\n",
					of, pp_parse.pp->pp0.hdr.tbl_fmt_rev,
					pp_parse.pp->pp0.hdr.tbl_content_rev);
		r = -ATB_ERR;
		goto unlock_mutex;
	}

	pp_parse.state_array = atb->adev.rom + of
					+ get_unaligned_le16(&pp_parse.pp->
							pp0.state_array_of);
	pp_parse.desc_array = atb->adev.rom + of
					+ get_unaligned_le16(&pp_parse.pp->pp0
								.desc_array_of);
	pp_parse.clk_array = atb->adev.rom + of
					+ get_unaligned_le16(&pp_parse.pp->pp0
							.clk_array_of);

	state_get(&pp_parse, emergency, is_emergency_state);

	r = 0;

unlock_mutex:
	mutex_unlock(&atb->mutex);
	return r;
	
}
EXPORT_SYMBOL_GPL(atb_pp_emergency_state_get);

static u8 is_ulv_state(struct pp_parse *pp_parse, struct pp_state *s)
{
	u16 class_1;
	u16 d_of;
	struct pp_desc *d;

	d_of = s->desc_idx * pp_parse->desc_array->entry_sz;
	d = (struct pp_desc*)((u8*)&pp_parse->desc_array->descs[0] + d_of);

	class_1 = get_unaligned_le16(&d->class_1);

	if (class_1 & PP_DESC_CLASS_1_ULV)
		return 1;
	return 0;
}

long atb_pp_ulv_state_get(struct atombios *atb, struct atb_pp_state *ulv)
{
	u16 of;
	struct master_data_tbl *data_tbl;
	struct pp_parse pp_parse;
	long r;

	mutex_lock(&atb->mutex);

	of = get_unaligned_le16(&atb->hdr->master_data_tbl_of);
	data_tbl = atb->adev.rom + of;

	of = get_unaligned_le16(&data_tbl->list.pp_info);
	pp_parse.pp = atb->adev.rom + of;

	dev_info(atb->adev.dev, "atombios:pp_info (0x%04x) revision %u.%u\n",
					of, pp_parse.pp->pp0.hdr.tbl_fmt_rev,
					pp_parse.pp->pp0.hdr.tbl_content_rev);

	if (pp_parse.pp->pp0.hdr.tbl_fmt_rev != 6
				&& pp_parse.pp->pp0.hdr.tbl_content_rev != 1) {
		dev_err(atb->adev.dev, "atombios:pp_info (0x%04x) revision %u.%u not supported\n",
					of, pp_parse.pp->pp0.hdr.tbl_fmt_rev,
					pp_parse.pp->pp0.hdr.tbl_content_rev);
		r = -ATB_ERR;
		goto unlock_mutex;
	}

	pp_parse.state_array = atb->adev.rom + of
					+ get_unaligned_le16(&pp_parse.pp->
							pp0.state_array_of);
	pp_parse.desc_array = atb->adev.rom + of
					+ get_unaligned_le16(&pp_parse.pp->pp0
								.desc_array_of);
	pp_parse.clk_array = atb->adev.rom + of
					+ get_unaligned_le16(&pp_parse.pp->pp0
							.clk_array_of);

	state_get(&pp_parse, ulv, is_ulv_state);

	r = 0;

unlock_mutex:
	mutex_unlock(&atb->mutex);
	return r;
	
}
EXPORT_SYMBOL_GPL(atb_pp_ulv_state_get);

static u8 is_performance_state(struct pp_parse *pp_parse, struct pp_state *s)
{
	u16 class_0;
	u16 d_of;
	struct pp_desc *d;

	d_of = s->desc_idx * pp_parse->desc_array->entry_sz;
	d = (struct pp_desc*)((u8*)&pp_parse->desc_array->descs[0] + d_of);

	class_0 = get_unaligned_le16(&d->class_0);
	class_0 &= PP_DESC_CLASS_UI_MASK;

	if (class_0 == PP_DESC_CLASS_UI_PERFORMANCE)
		return 1;
	return 0;
}

long atb_pp_performance_state_get(struct atombios *atb,
					struct atb_pp_state *performance)
{
	u16 of;
	struct master_data_tbl *data_tbl;
	struct pp_parse pp_parse;
	long r;

	mutex_lock(&atb->mutex);

	of = get_unaligned_le16(&atb->hdr->master_data_tbl_of);
	data_tbl = atb->adev.rom + of;

	of = get_unaligned_le16(&data_tbl->list.pp_info);
	pp_parse.pp = atb->adev.rom + of;

	dev_info(atb->adev.dev, "atombios:pp_info (0x%04x) revision %u.%u\n",
					of, pp_parse.pp->pp0.hdr.tbl_fmt_rev,
					pp_parse.pp->pp0.hdr.tbl_content_rev);

	if (pp_parse.pp->pp0.hdr.tbl_fmt_rev != 6
				&& pp_parse.pp->pp0.hdr.tbl_content_rev != 1) {
		dev_err(atb->adev.dev, "atombios:pp_info (0x%04x) revision %u.%u not supported\n",
					of, pp_parse.pp->pp0.hdr.tbl_fmt_rev,
					pp_parse.pp->pp0.hdr.tbl_content_rev);
		r = -ATB_ERR;
		goto unlock_mutex;
	}

	pp_parse.state_array = atb->adev.rom + of
					+ get_unaligned_le16(&pp_parse.pp->
							pp0.state_array_of);
	pp_parse.desc_array = atb->adev.rom + of
					+ get_unaligned_le16(&pp_parse.pp->pp0
								.desc_array_of);
	pp_parse.clk_array = atb->adev.rom + of
					+ get_unaligned_le16(&pp_parse.pp->pp0
							.clk_array_of);

	state_get(&pp_parse, performance, is_performance_state);

	r = 0;

unlock_mutex:
	mutex_unlock(&atb->mutex);
	return r;
	
}
EXPORT_SYMBOL_GPL(atb_pp_performance_state_get);

/* have thermal protection only if we have the proper internal thermal ctrler */
long atb_have_thermal_protection(struct atombios *atb)
{
	u16 of;
	struct master_data_tbl *data_tbl;
	union pp *pp;
	long r;

	mutex_lock(&atb->mutex);

	of = get_unaligned_le16(&atb->hdr->master_data_tbl_of);
	data_tbl = atb->adev.rom + of;

	of = get_unaligned_le16(&data_tbl->list.pp_info);
	pp = atb->adev.rom + of;

	dev_info(atb->adev.dev, "atombios:pp_info (0x%04x) revision %u.%u\n",
		of, pp->pp0.hdr.tbl_fmt_rev, pp->pp0.hdr.tbl_content_rev);

	if (pp->pp0.hdr.tbl_fmt_rev != 6 && pp->pp0.hdr.tbl_content_rev != 1) {
		dev_err(atb->adev.dev, "atombios:pp_info (0x%04x) revision %u.%u not supported\n",
						of, pp->pp0.hdr.tbl_fmt_rev,
						pp->pp0.hdr.tbl_content_rev);
		r = -ATB_ERR;
		goto unlock_mutex;
	}

	r = ATB_HAVE_THERMAL_PROTECTION;
	if (pp->pp0.thermal_ctrler.type != PP_THERMAL_CTRLER_SI)
		r = ATB_DONT_HAVE_THERMAL_PROTECTION;
unlock_mutex:
	mutex_unlock(&atb->mutex);
	return r;
}
EXPORT_SYMBOL_GPL(atb_have_thermal_protection);

long pp_platform_caps_get(struct atombios *atb, u32 *platform_caps)
{
	u16 of;
	struct master_data_tbl *data_tbl;
	union pp *pp;
	long r;

	of = get_unaligned_le16(&atb->hdr->master_data_tbl_of);
	data_tbl = atb->adev.rom + of;

	of = get_unaligned_le16(&data_tbl->list.pp_info);
	pp = atb->adev.rom + of;

	dev_info(atb->adev.dev, "atombios:pp_info (0x%04x) revision %u.%u\n",
		of, pp->pp0.hdr.tbl_fmt_rev, pp->pp0.hdr.tbl_content_rev);

	if (pp->pp0.hdr.tbl_fmt_rev != 6 && pp->pp0.hdr.tbl_content_rev != 1) {
		dev_err(atb->adev.dev, "atombios:pp_info (0x%04x) revision %u.%u not supported\n",
						of, pp->pp0.hdr.tbl_fmt_rev,
						pp->pp0.hdr.tbl_content_rev);
		r = -ATB_ERR;
		goto exit;
	}

	*platform_caps = get_unaligned_le32(&pp->pp0.platform_caps);

	r = 0;
	
exit:
	return r;
}

long atb_pp_platform_caps_get(struct atombios *atb, u32 *platform_caps)
{
	long r;

	mutex_lock(&atb->mutex);

	r = pp_platform_caps_get(atb, platform_caps);
	
	mutex_unlock(&atb->mutex);
	return r;
}
EXPORT_SYMBOL_GPL(atb_pp_platform_caps_get);

long atb_pp_back_bias_time_get(struct atombios *atb, u16 *back_bias_time)
{
	u16 of;
	struct master_data_tbl *data_tbl;
	union pp *pp;
	long r;

	mutex_lock(&atb->mutex);

	of = get_unaligned_le16(&atb->hdr->master_data_tbl_of);
	data_tbl = atb->adev.rom + of;

	of = get_unaligned_le16(&data_tbl->list.pp_info);
	pp = atb->adev.rom + of;

	dev_info(atb->adev.dev, "atombios:pp_info (0x%04x) revision %u.%u\n",
		of, pp->pp0.hdr.tbl_fmt_rev, pp->pp0.hdr.tbl_content_rev);

	if (pp->pp0.hdr.tbl_fmt_rev != 6 && pp->pp0.hdr.tbl_content_rev != 1) {
		dev_err(atb->adev.dev, "atombios:pp_info (0x%04x) revision %u.%u not supported\n",
						of, pp->pp0.hdr.tbl_fmt_rev,
						pp->pp0.hdr.tbl_content_rev);
		r = -ATB_ERR;
		goto unlock_mutex;
	}

	*back_bias_time = get_unaligned_le16(&pp->pp0.back_bias_time);

	r = 0;
	
unlock_mutex:
	mutex_unlock(&atb->mutex);
	return r;
}
EXPORT_SYMBOL_GPL(atb_pp_back_bias_time_get);

long atb_pp_tdp_limits_get(struct atombios *atb, u32 *tdp_limit,
							u32 *near_tdp_limit)
{
	u16 of;
	struct master_data_tbl *data_tbl;
	union pp *pp;
	long r;

	mutex_lock(&atb->mutex);

	of = get_unaligned_le16(&atb->hdr->master_data_tbl_of);
	data_tbl = atb->adev.rom + of;

	of = get_unaligned_le16(&data_tbl->list.pp_info);
	pp = atb->adev.rom + of;

	dev_info(atb->adev.dev, "atombios:pp_info (0x%04x) revision %u.%u\n",
		of, pp->pp0.hdr.tbl_fmt_rev, pp->pp0.hdr.tbl_content_rev);

	if (pp->pp0.hdr.tbl_fmt_rev != 6 && pp->pp0.hdr.tbl_content_rev != 1) {
		dev_err(atb->adev.dev, "atombios:pp_info (0x%04x) revision %u.%u not supported\n",
						of, pp->pp0.hdr.tbl_fmt_rev,
						pp->pp0.hdr.tbl_content_rev);
		r = -ATB_ERR;
		goto unlock_mutex;
	}

	*tdp_limit = get_unaligned_le32(&pp->pp4.tdp_limit);
	*near_tdp_limit = get_unaligned_le32(&pp->pp4.near_tdp_limit);

	r = 0;
	
unlock_mutex:
	mutex_unlock(&atb->mutex);
	return r;
}
EXPORT_SYMBOL_GPL(atb_pp_tdp_limits_get);

long atb_pp_sq_ramping_threshold_get(struct atombios *atb,
						u32 *sq_ramping_threshold)
{
	u16 of;
	struct master_data_tbl *data_tbl;
	union pp *pp;
	u16 tbl_sz;
	long r;

	mutex_lock(&atb->mutex);

	of = get_unaligned_le16(&atb->hdr->master_data_tbl_of);
	data_tbl = atb->adev.rom + of;

	of = get_unaligned_le16(&data_tbl->list.pp_info);
	pp = atb->adev.rom + of;

	dev_info(atb->adev.dev, "atombios:pp_info (0x%04x) revision %u.%u\n",
		of, pp->pp0.hdr.tbl_fmt_rev, pp->pp0.hdr.tbl_content_rev);

	if (pp->pp0.hdr.tbl_fmt_rev != 6 && pp->pp0.hdr.tbl_content_rev != 1) {
		dev_err(atb->adev.dev, "atombios:pp_info (0x%04x) revision %u.%u not supported\n",
						of, pp->pp0.hdr.tbl_fmt_rev,
						pp->pp0.hdr.tbl_content_rev);
		r = -ATB_ERR;
		goto unlock_mutex;
	}

	tbl_sz = get_unaligned_le16(&pp->pp0.tbl_sz);
	if (tbl_sz < sizeof(struct pp4)) {
		dev_err(atb->adev.dev, "atombios:missing powerplay 4 table\n");
		r = -ATB_ERR;
		goto unlock_mutex;
	}

	*sq_ramping_threshold = get_unaligned_le32(
						&pp->pp4.sq_ramping_threshold);

	r = 0;
	
unlock_mutex:
	mutex_unlock(&atb->mutex);
	return r;
}
EXPORT_SYMBOL_GPL(atb_pp_sq_ramping_threshold_get);
